{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "9eba78b3",
   "metadata": {},
   "source": [
    "# Chapter 4 - Markov Chains and Networks (Python Code)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0c5585b0",
   "metadata": {
    "tags": [
     "hide-output"
    ]
   },
   "outputs": [],
   "source": [
    "! pip install --upgrade quantecon_book_networks"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03d19647",
   "metadata": {},
   "source": [
    "We begin with some imports."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "48b566bb",
   "metadata": {},
   "outputs": [],
   "source": [
    "import quantecon as qe\n",
    "import quantecon_book_networks\n",
    "import quantecon_book_networks.input_output as qbn_io\n",
    "import quantecon_book_networks.plotting as qbn_plt\n",
    "import quantecon_book_networks.data as qbn_data\n",
    "ch4_data = qbn_data.markov_chains_and_networks()\n",
    "export_figures = False"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d76606ee",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "import networkx as nx\n",
    "import matplotlib.pyplot as plt\n",
    "from matplotlib import cm\n",
    "from mpl_toolkits.mplot3d import Axes3D\n",
    "from mpl_toolkits.mplot3d.art3d import Poly3DCollection\n",
    "quantecon_book_networks.config(\"matplotlib\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "da06eb17",
   "metadata": {},
   "source": [
    "## Example transition matrices\n",
    "\n",
    "In this chapter two transition matrices are used.\n",
    "\n",
    "First, a Markov model is estimated in the international growth dynamics study\n",
    "of [Quah (1993)](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.142.5504&rep=rep1&type=pdf).\n",
    "\n",
    "The state is real GDP per capita in a given country relative to the world\n",
    "average. \n",
    "\n",
    "Quah discretizes the possible values to 0–1/4, 1/4–1/2, 1/2–1, 1–2\n",
    "and 2–inf, calling these states 1 to 5 respectively. The transitions are over\n",
    "a one year period."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b1d19891",
   "metadata": {},
   "outputs": [],
   "source": [
    "P_Q = [\n",
    "    [0.97, 0.03, 0,    0,    0   ],\n",
    "    [0.05, 0.92, 0.03, 0,    0   ],\n",
    "    [0,    0.04, 0.92, 0.04, 0   ],\n",
    "    [0,    0,    0.04, 0.94, 0.02],\n",
    "    [0,    0,    0,    0.01, 0.99]\n",
    "]\n",
    "P_Q = np.array(P_Q)\n",
    "codes_Q =  ('1', '2', '3', '4', '5')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c9d3f18b",
   "metadata": {},
   "source": [
    "Second, [Benhabib et al. (2015)](https://www.economicdynamics.org/meetpapers/2015/paper_364.pdf) estimate the following transition matrix for intergenerational social mobility.\n",
    "\n",
    "The states are percentiles of the wealth distribution. \n",
    "\n",
    "In particular, states 1, 2,..., 8, correspond to the percentiles 0-20%, 20-40%, 40-60%, 60-80%, 80-90%, 90-95%, 95-99%, 99-100%."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8a2ff2d3",
   "metadata": {},
   "outputs": [],
   "source": [
    "P_B = [\n",
    "    [0.222, 0.222, 0.215, 0.187, 0.081, 0.038, 0.029, 0.006],\n",
    "    [0.221, 0.22,  0.215, 0.188, 0.082, 0.039, 0.029, 0.006],\n",
    "    [0.207, 0.209, 0.21,  0.194, 0.09,  0.046, 0.036, 0.008],\n",
    "    [0.198, 0.201, 0.207, 0.198, 0.095, 0.052, 0.04,  0.009],\n",
    "    [0.175, 0.178, 0.197, 0.207, 0.11,  0.067, 0.054, 0.012],\n",
    "    [0.182, 0.184, 0.2,   0.205, 0.106, 0.062, 0.05,  0.011],\n",
    "    [0.123, 0.125, 0.166, 0.216, 0.141, 0.114, 0.094, 0.021],\n",
    "    [0.084, 0.084, 0.142, 0.228, 0.17,  0.143, 0.121, 0.028]\n",
    "    ]\n",
    "\n",
    "P_B = np.array(P_B)\n",
    "codes_B =  ('1', '2', '3', '4', '5', '6', '7', '8')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "53bd61df",
   "metadata": {},
   "source": [
    "## Markov Chains as Digraphs\n",
    "\n",
    "### Contour plot of a transition matrix \n",
    "\n",
    "Here we define a function for producing contour plots of matrices."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a95d7285",
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_matrices(matrix,\n",
    "                  codes,\n",
    "                  ax,\n",
    "                  font_size=12,\n",
    "                  alpha=0.6, \n",
    "                  colormap=cm.viridis, \n",
    "                  color45d=None, \n",
    "                  xlabel='sector $j$', \n",
    "                  ylabel='sector $i$'):\n",
    "    \n",
    "    ticks = range(len(matrix))\n",
    "\n",
    "    levels = np.sqrt(np.linspace(0, 0.75, 100))\n",
    "    \n",
    "    \n",
    "    if color45d != None:\n",
    "        co = ax.contourf(ticks, \n",
    "                         ticks,\n",
    "                         matrix,\n",
    "                         alpha=alpha, cmap=colormap)\n",
    "        ax.plot(ticks, ticks, color=color45d)\n",
    "    else:\n",
    "        co = ax.contourf(ticks, \n",
    "                         ticks,\n",
    "                         matrix,\n",
    "                         levels,\n",
    "                         alpha=alpha, cmap=colormap)\n",
    "\n",
    "    ax.set_xlabel(xlabel, fontsize=font_size)\n",
    "    ax.set_ylabel(ylabel, fontsize=font_size)\n",
    "    ax.set_yticks(ticks)\n",
    "    ax.set_yticklabels(codes_B)\n",
    "    ax.set_xticks(ticks)\n",
    "    ax.set_xticklabels(codes_B)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "15a6af09",
   "metadata": {},
   "source": [
    "Now we use our function to produce a plot of the transition matrix for\n",
    "intergenerational social mobility, $P_B$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8dc01df7",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots(figsize=(6,6))\n",
    "plot_matrices(P_B.transpose(), codes_B, ax, alpha=0.75, \n",
    "                 colormap=cm.viridis, color45d='black',\n",
    "                 xlabel='state at time $t$', ylabel='state at time $t+1$')\n",
    "\n",
    "if export_figures:\n",
    "    plt.savefig(\"figures/markov_matrix_visualization.pdf\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b2476ede",
   "metadata": {},
   "source": [
    "### Wealth percentile over time\n",
    "\n",
    "Here we compare the mixing of the transition matrix for intergenerational\n",
    "social mobility $P_B$ and the transition matrix for international growth\n",
    "dynamics $P_Q$. \n",
    "\n",
    "We begin by creating `quantecon` `MarkovChain` objects with each of our transition\n",
    "matrices."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f13e25bb",
   "metadata": {},
   "outputs": [],
   "source": [
    "mc_B = qe.MarkovChain(P_B, state_values=range(1, 9))\n",
    "mc_Q = qe.MarkovChain(P_Q, state_values=range(1, 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "df645ec9",
   "metadata": {},
   "source": [
    "Next we define a function to plot simulations of Markov chains. \n",
    "\n",
    "Two simulations will be run for each `MarkovChain`, one starting at the\n",
    "minimum initial value and one at the maximum."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "174ec3fc",
   "metadata": {},
   "outputs": [],
   "source": [
    "def sim_fig(ax, mc, T=100, seed=14, title=None):\n",
    "    X1 = mc.simulate(T, init=1, random_state=seed)\n",
    "    X2 = mc.simulate(T, init=max(mc.state_values), random_state=seed+1)\n",
    "    ax.plot(X1)\n",
    "    ax.plot(X2)\n",
    "    ax.set_xlabel(\"time\")\n",
    "    ax.set_ylabel(\"state\")\n",
    "    ax.set_title(title, fontsize=12)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "05dc505f",
   "metadata": {},
   "source": [
    "Finally, we produce the figure."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6c98817b",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, axes = plt.subplots(2, 1, figsize=(6, 4))\n",
    "ax = axes[0]\n",
    "sim_fig(axes[0], mc_B, title=\"$P_B$\")\n",
    "sim_fig(axes[1], mc_Q, title=\"$P_Q$\")\n",
    "axes[1].set_yticks((1, 2, 3, 4, 5))\n",
    "\n",
    "plt.tight_layout()\n",
    "if export_figures:\n",
    "    plt.savefig(\"figures/benhabib_mobility_mixing.pdf\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2bf229d0",
   "metadata": {},
   "source": [
    "### Predicted vs realized cross-country income distributions for 2019\n",
    "\n",
    "Here we load a `pandas` `DataFrame` of GDP per capita data for countries compared to the global average."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "44dc111f",
   "metadata": {},
   "outputs": [],
   "source": [
    "gdppc_df = ch4_data['gdppc_df']\n",
    "gdppc_df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d3a50768",
   "metadata": {},
   "source": [
    "Now we assign countries bins, as per Quah (1993)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "09775bab",
   "metadata": {},
   "outputs": [],
   "source": [
    "q = [0, 0.25, 0.5, 1.0, 2.0, np.inf]\n",
    "l = [0, 1, 2, 3, 4]\n",
    "\n",
    "x = pd.cut(gdppc_df.gdppc_r, bins=q, labels=l)\n",
    "gdppc_df['interval'] = x\n",
    "\n",
    "gdppc_df = gdppc_df.reset_index()\n",
    "gdppc_df['interval'] = gdppc_df['interval'].astype(float)\n",
    "gdppc_df['year'] = gdppc_df['year'].astype(float)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8c91bb21",
   "metadata": {},
   "source": [
    "Here we define a function for calculating the cross-country income\n",
    "distributions for a given date range."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e7e86382",
   "metadata": {},
   "outputs": [],
   "source": [
    "def gdp_dist_estimate(df, l, yr=(1960, 2019)):\n",
    "    Y = np.zeros(len(l))\n",
    "    for i in l:\n",
    "        Y[i] = df[\n",
    "            (df['interval'] == i) & \n",
    "            (df['year'] <= yr[1]) & \n",
    "            (df['year'] >= yr[0])\n",
    "            ].count()[0]\n",
    "    \n",
    "    return Y / Y.sum()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ad6d54d7",
   "metadata": {},
   "source": [
    "We calculate the true distribution for 1985."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "130b145a",
   "metadata": {},
   "outputs": [],
   "source": [
    "ψ_1985 = gdp_dist_estimate(gdppc_df,l,yr=(1985, 1985))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b3b24523",
   "metadata": {},
   "source": [
    "Now we use the transition matrix to update the 1985 distribution $t = 2019 - 1985 = 34$\n",
    "times to get our predicted 2019 distribution."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "de8e360b",
   "metadata": {},
   "outputs": [],
   "source": [
    "ψ_2019_predicted = ψ_1985 @ np.linalg.matrix_power(P_Q, 2019-1985)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3e8be48f",
   "metadata": {},
   "source": [
    "Now, calculate the true 2019 distribution."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "48653d34",
   "metadata": {},
   "outputs": [],
   "source": [
    "ψ_2019 = gdp_dist_estimate(gdppc_df,l,yr=(2019, 2019))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "886af82b",
   "metadata": {},
   "source": [
    "Finally we produce the plot."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0d6c8426",
   "metadata": {},
   "outputs": [],
   "source": [
    "states = np.arange(0, 5)\n",
    "ticks = range(5)\n",
    "codes_S = ('1', '2', '3', '4', '5')\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(6, 4))\n",
    "width = 0.4\n",
    "ax.plot(states, ψ_2019_predicted, '-o', alpha=0.7, label='predicted')\n",
    "ax.plot(states, ψ_2019, '-o', alpha=0.7, label='realized')\n",
    "ax.set_xlabel(\"state\")\n",
    "ax.set_ylabel(\"probability\")\n",
    "ax.set_yticks((0.15, 0.2, 0.25, 0.3))\n",
    "ax.set_xticks(ticks)\n",
    "ax.set_xticklabels(codes_S)\n",
    "\n",
    "ax.legend(loc='upper center', fontsize=12)\n",
    "if export_figures:\n",
    "    plt.savefig(\"figures/quah_gdppc_prediction.pdf\")\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce838e58",
   "metadata": {},
   "source": [
    "### Distribution dynamics\n",
    "\n",
    "Here we define a function for plotting the convergence of marginal\n",
    "distributions $\\psi$ under a transition matrix $P$ on the unit simplex."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6eaa5db8",
   "metadata": {},
   "outputs": [],
   "source": [
    "def convergence_plot(ψ, P, n=14, angle=50):\n",
    "\n",
    "    ax = qbn_plt.unit_simplex(angle)\n",
    "\n",
    "    # Convergence plot\n",
    "    \n",
    "    P = np.array(P)\n",
    "\n",
    "    ψ = ψ        # Initial condition\n",
    "\n",
    "    x_vals, y_vals, z_vals = [], [], []\n",
    "    for t in range(n):\n",
    "        x_vals.append(ψ[0])\n",
    "        y_vals.append(ψ[1])\n",
    "        z_vals.append(ψ[2])\n",
    "        ψ = ψ @ P\n",
    "\n",
    "    ax.scatter(x_vals, y_vals, z_vals, c='darkred', s=80, alpha=0.7, depthshade=False)\n",
    "\n",
    "    mc = qe.MarkovChain(P)\n",
    "    ψ_star = mc.stationary_distributions[0]\n",
    "    ax.scatter(ψ_star[0], ψ_star[1], ψ_star[2], c='k', s=80)\n",
    "\n",
    "    return ψ\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5bc093e3",
   "metadata": {},
   "source": [
    "Now we define P."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "64d602c8",
   "metadata": {},
   "outputs": [],
   "source": [
    "P = (\n",
    "    (0.9, 0.1, 0.0),\n",
    "    (0.4, 0.4, 0.2),\n",
    "    (0.1, 0.1, 0.8)\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a38c94cd",
   "metadata": {},
   "source": [
    "#### A trajectory from $\\psi_0 = (0, 0, 1)$\n",
    "\n",
    "Here we see the sequence of marginals appears to converge."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "39d90e91",
   "metadata": {},
   "outputs": [],
   "source": [
    "ψ_0 = (0, 0, 1)\n",
    "ψ = convergence_plot(ψ_0, P)\n",
    "if export_figures:\n",
    "    plt.savefig(\"figures/simplex_2.pdf\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "da3337df",
   "metadata": {},
   "source": [
    "#### A trajectory from $\\psi_0 = (0, 1/2, 1/2)$\n",
    "\n",
    "Here we see again that the sequence of marginals appears to converge, and the\n",
    "limit appears not to depend on the initial distribution."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c4ba2e7b",
   "metadata": {},
   "outputs": [],
   "source": [
    "ψ_0 = (0, 1/2, 1/2)\n",
    "ψ = convergence_plot(ψ_0, P, n=12)\n",
    "if export_figures:\n",
    "    plt.savefig(\"figures/simplex_3.pdf\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7491cf30",
   "metadata": {},
   "source": [
    "### Distribution projections from $P_B$\n",
    "\n",
    "Here we define a function for plotting $\\psi$ after $n$ iterations of the\n",
    "transition matrix $P$. The distribution $\\psi_0$ is taken as the uniform \n",
    "distribution over the\n",
    "state space."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6dd25c98",
   "metadata": {},
   "outputs": [],
   "source": [
    "def transition(P, n, ax=None):\n",
    "    \n",
    "    P = np.array(P)\n",
    "    nstates = P.shape[1]\n",
    "    s0 = np.ones(8) * 1/nstates\n",
    "    s = s0\n",
    "    \n",
    "    for i in range(n):\n",
    "        s = s @ P\n",
    "        \n",
    "    if ax is None:\n",
    "        fig, ax = plt.subplots()\n",
    "        \n",
    "    ax.plot(range(1, nstates+1), s, '-o', alpha=0.6)\n",
    "    ax.set(ylim=(0, 0.25), \n",
    "           xticks=((1, nstates)))\n",
    "    ax.set_title(f\"$t = {n}$\")\n",
    "    \n",
    "    return ax"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "659480d1",
   "metadata": {},
   "source": [
    "We now generate the marginal distributions after 0, 1, 2, and 100 iterations for $P_B$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a1bbb0cd",
   "metadata": {},
   "outputs": [],
   "source": [
    "ns = (0, 1, 2, 100)\n",
    "fig, axes = plt.subplots(1, len(ns), figsize=(6, 4))\n",
    "\n",
    "for n, ax in zip(ns, axes):\n",
    "    ax = transition(P_B, n, ax=ax)\n",
    "    ax.set_xlabel(\"Quantile\")\n",
    "\n",
    "plt.tight_layout()\n",
    "if export_figures:\n",
    "    plt.savefig(\"figures/benhabib_mobility_dists.pdf\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e7ee0c57",
   "metadata": {},
   "source": [
    "## Asymptotics\n",
    "\n",
    "### Convergence of the empirical distribution to $\\psi^*$\n",
    "\n",
    "We begin by creating a `MarkovChain` object, taking $P_B$ as the transition matrix."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2f911e34",
   "metadata": {},
   "outputs": [],
   "source": [
    "mc = qe.MarkovChain(P_B)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7ca9220f",
   "metadata": {},
   "source": [
    "Next we use the `quantecon` package to calculate the true stationary distribution."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "50947355",
   "metadata": {},
   "outputs": [],
   "source": [
    "stationary = mc.stationary_distributions[0]\n",
    "n = len(mc.P)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0ea1a5bf",
   "metadata": {},
   "source": [
    "Now we define a function to simulate the Markov chain."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d98416ff",
   "metadata": {},
   "outputs": [],
   "source": [
    "def simulate_distribution(mc, T=100):\n",
    "    # Simulate path \n",
    "    n = len(mc.P)\n",
    "    path = mc.simulate_indices(ts_length=T, random_state=1)\n",
    "    distribution = np.empty(n)\n",
    "    for i in range(n):\n",
    "        distribution[i] = np.mean(path==i)\n",
    "    return distribution\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7bd6bdaa",
   "metadata": {},
   "source": [
    "We run simulations of length 10, 100, 1,000 and 10,000."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bef22ec7",
   "metadata": {},
   "outputs": [],
   "source": [
    "lengths = [10, 100, 1_000, 10_000]\n",
    "dists = []\n",
    "\n",
    "for t in lengths:\n",
    "    dists.append(simulate_distribution(mc, t))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a185b18b",
   "metadata": {},
   "source": [
    "Now we produce the plots. \n",
    "\n",
    "We see that the simulated distribution starts to approach the true stationary distribution."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6ccd5bdf",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, axes = plt.subplots(2, 2, figsize=(9, 6), sharex='all')#, sharey='all')\n",
    "\n",
    "axes = axes.flatten()\n",
    "\n",
    "for dist, ax, t in zip(dists, axes, lengths):\n",
    "    \n",
    "    ax.plot(np.arange(n)+1 + .25, \n",
    "           stationary, \n",
    "            '-o',\n",
    "           #width = 0.25, \n",
    "           label='$\\\\psi^*$', \n",
    "           alpha=0.75)\n",
    "    \n",
    "    ax.plot(np.arange(n)+1, \n",
    "           dist, \n",
    "            '-o',\n",
    "           #width = 0.25, \n",
    "           label=f'$\\\\hat \\\\psi_k$ with $k={t}$', \n",
    "           alpha=0.75)\n",
    "\n",
    "\n",
    "    ax.set_xlabel(\"state\", fontsize=12)\n",
    "    ax.set_ylabel(\"prob.\", fontsize=12)\n",
    "    ax.set_xticks(np.arange(n)+1)\n",
    "    ax.legend(loc='upper right', fontsize=12, frameon=False)\n",
    "    ax.set_ylim(0, 0.5)\n",
    "    \n",
    "if export_figures:\n",
    "    plt.savefig(\"figures/benhabib_ergodicity_1.pdf\")\n",
    "plt.show()"
   ]
  }
 ],
 "metadata": {
  "jupytext": {
   "cell_metadata_filter": "-all"
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
